package de.justphil.tcg.tcgserver.game;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.glassfish.grizzly.websockets.WebSocket;
import org.slf4j.Logger;

import com.hazelcast.nio.DataSerializable;

import static de.justphil.tcg.tcgserver.commons.logging.TCGLogging.*;
import de.justphil.tcg.tcgserver.commons.config.GameConfig;
import de.justphil.tcg.tcgserver.commons.domain.Card;
import de.justphil.tcg.tcgserver.commons.domain.collections.CardInstanceCollection;
import de.justphil.tcg.tcgserver.game.domain.Player;
import de.justphil.tcg.tcgserver.game.helpers.GameHelper;
import de.justphil.tcg.tcgserver.game.helpers.GameReporter;
import de.justphil.tcg.tcgserver.game.interfaces.Progressing;
import de.justphil.tcg.tcgserver.game.mechanics.game.Game;
import de.justphil.tcg.tcgserver.game.mechanics.game.OnGameProgressListener;
import de.justphil.tcg.tcgserver.game.mechanics.game.OnUserInteractionListener;
import de.justphil.tcg.tcgserver.game.states.GameState;
import de.justphil.tcg.tcgserver.game.states.WaitingForPlayerInteraction;
import de.justphil.tcg.tcgserver.game.states.WaitingForPlayers;
import de.justphil.tcg.tcgserver.game.states.WaitingForReconnectingPlayer;
import de.justphil.tcg.tcgserver.websockets.TCGWebSocket;
import de.justphil.tcg.tcgserver.websockets.WebSocketApplicationHolder;
import de.justphil.tcg.tcgserver.commons.i18n.M;
import de.justphil.tcg.tcgserver.websockets.adapters.DeckAdapter;
import de.justphil.tcg.tcgserver.websockets.interfaces.TCGAppManageable;
import de.justphil.tcg.tcgserver.websockets.messages.TCGMessage;
import de.justphil.tcg.tcgserver.websockets.messages.TCGProgressFragment;


public class GameManager implements Progressing, OnGameProgressListener, DataSerializable {
	private static final long serialVersionUID = 4290431473233442154L;
	private static final Logger log = logger(GameManager.class);
	
	private TCGAppManageable tcgAppManageable;
	private long gameId;
	private List<Player> players;
	private int activePlayerIndex;
	private GameState state;
	private OnUserInteractionListener game;
	private boolean[] resumeRequired;
	private Map<Long, Long> ciIdToCardIdMapping;
	private String gameOtp;
	
	public GameManager() {
		// just to conform the DataSerializable interface
	}

	public GameManager(TCGAppManageable tcgAppManageable, long gameId, List<Long> playerIds,
			CardInstanceCollection[] decks, String[] otps, Map<Long, Integer> playerToDeckMapping,
			Map<Long, Integer> playerToOtpMapping, Map<Long, Card> cardMap, String gameOtp) {
		this.tcgAppManageable 	= tcgAppManageable;
		this.gameId				= gameId;
		this.players			= createPlayers(playerIds, otps, playerToDeckMapping, playerToOtpMapping);
		this.activePlayerIndex 	= 0;
		this.resumeRequired 	= new boolean[GameConfig.NUMBER_OF_PLAYERS_PER_GAME];
		
		for(int i = 0; i < resumeRequired.length; i++) {
			resumeRequired[i] = false;
		}
		
		DeckAdapter deckAdapter = new DeckAdapter();
		
		this.gameOtp = gameOtp;
		this.ciIdToCardIdMapping = deckAdapter.createCardInstanceIdToCardIdMapping( decks );
		this.game		= new Game( deckAdapter.adaptDecks( decks, cardMap ) );
		((Game) this.game).addOnGameProgressListener(this, false);
		setState(new WaitingForPlayers());
	}
	
	/* ############################################################################################ */
	/* ############################################################################################ */
	/* ############################################################################################ */
	
	@Override
	public void progress(WebSocket socket, TCGMessage msg, String data) {
		/*
		try {
			System.out.println(Thread.currentThread().getName() + ": Going to sleep for 5 sec...");
			Thread.sleep(5000);
			System.out.println(Thread.currentThread().getName() + ": Back again!");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		*/
		
		state.gameProgress(this, (TCGWebSocket) socket, msg, data);
	}
	
//	@Override
//	public void onConnectionProblem(WebSocket socket) {
//		Player player = getPlayerByWebSocket(socket);
//		
//		/*
//		if (player == getActivePlayer()) {
//			GameState newState = new WaitingForPlayers();
//			Log.log(getClass().getSimpleName(), "State transition in game '" + getGameId() + "': "+state+" -> "+newState);
//			setState(newState);
//		}
//		*/
//		
//		GameReporter.broadcastGameInfo(this, player, M.CONNECTION_PROBLEM);
//		
//		// TODO: GameHelper.commitGameChange(this);
//	}
	
//	@Override
//	public void onConnectionRehab(WebSocket socket) {
//		Player player = getPlayerByWebSocket(socket);
//		
//		/*
//		if (player == getActivePlayer() && state instanceof WaitingForPlayers) {
//			GameState newState = new WaitingForPlayerInteraction();
//			Log.log(getClass().getSimpleName(), "State transition in game '" + getGameId() + "': "+state+" -> "+newState);
//			setState(newState);
//		}
//		*/
//		
//		// TODO: Here some more logic is needed...
//		
//		GameReporter.broadcastGameInfo(this, player, M.CONNECTION_REHAB);
//		// TODO: GameHelper.commitGameChange(this);
//	}
	
	@Override
	public void onClose(WebSocket socket) {
		Player disconnectedPlayer = getPlayerByWebSocket(socket);
		
		assert disconnectedPlayer != null :
					"TCGApplication forwarded a WebSocket that is NOT associated with a player of this game!";
		
		GameHelper.cleanUpAbortedPlayerConnection(this, disconnectedPlayer, (TCGWebSocket) socket);
		
		if (state instanceof WaitingForPlayerInteraction) {
			// game is running, all players are connected
			log.info("State transition in game '{}': {} -> WaitingForReconnectingPlayer", getGameId(), state);
			setState(new WaitingForReconnectingPlayer());
		}
		
		GameReporter.broadcastGameInfo(this, null, M.WAITING_FOR_RECONNECT);
		
		GameHelper.commitGameChange(this);
	}
	
	@Override
	public void onReconnectTimeOut(long playerId) {
		GameReporter.broadcastGameInfo(this, null, M.RECONNECT_TIMEOUT);
		
		Player disconnectedPlayer = getPlayerById(playerId);
		Player opponent = getOpponent(disconnectedPlayer);
		
		onGameOver(opponent.getIndex());
		
		// No need to commit game change because onGameOver() has killed the game!
	}
	
	@Override
	public void onGameExpired() {
		GameReporter.broadcastGameInfo(this, null, M.GAME_EXPIRED);
		
		tcgAppManageable.signalGameExpired(getGameId(), getGameOtp());
		tellTCGManageableToCleanUp();
	}
	
	/* ############################################################################################ */
	/* ############################################################################################ */
	/* ############################################################################################ */
	
	public void setState(GameState state) {
		this.state = state;
	}
	
	public String getStateAsString() {
		return state.toString();
	}
	
	/* ############################################################################################ */
	/* ############################################################################################ */
	/* ############################################################################################ */
	
	@Override
	public void readData(DataInput in) throws IOException {
		log.info("readData()");
		
		tcgAppManageable = WebSocketApplicationHolder.getTCGApplication();
		gameId = in.readLong();
		
		int playerCount = in.readInt();
		players = new ArrayList<Player>(playerCount);
		Player p;
		for(int i = 0; i < playerCount; i++) {
			p = new Player();
			p.readData(in);
			players.add(p);
		}
		
		activePlayerIndex = in.readInt();
		
		// recover state
		try {
			String className = in.readUTF();
			Class<?> clazz = Class.forName(className);
			Constructor<?> c = clazz.getConstructor(new Class[0]);
			state = (GameState) c.newInstance(new Object[0]);
		}
		catch (Exception e) {
			log.warn("Exception occured during deserialization of 'state'!");
		}
		
		// recover game mechanics
		game = new Game();
		((DataSerializable) game).readData(in);
		((Game) game).addOnGameProgressListener(this, false);
		
		// recover resumeRequired array
		resumeRequired = new boolean[in.readInt()];
		for(int i = 0; i < resumeRequired.length; i++) {
			resumeRequired[i] = in.readBoolean();
		}
		
		// recover ciIdToCardIdMapping
		int mappingSize = in.readInt();
		ciIdToCardIdMapping = new HashMap<Long, Long>(mappingSize);
		long ciId, cardId;
		for (int i = 0; i < mappingSize; i++) {
			ciId = in.readLong();
			cardId = in.readLong();
			ciIdToCardIdMapping.put(ciId, cardId);
		}
		
		gameOtp = in.readUTF();
	}

	@Override
	public void writeData(DataOutput out) throws IOException {
		log.info("writeData()");
		
		out.writeLong(gameId);
		
		out.writeInt(players.size());
		for (Player player : players) {
			player.writeData(out);
		}
		
		out.writeInt(activePlayerIndex);
		
		out.writeUTF(state.getClass().getCanonicalName());
		
		((DataSerializable) game).writeData(out);
		
		out.writeInt(resumeRequired.length);
		for(int i = 0; i < resumeRequired.length; i++) {
			out.writeBoolean(resumeRequired[i]);
		}
		
		out.writeInt(ciIdToCardIdMapping.size());
		for (Long ciId : ciIdToCardIdMapping.keySet()) {
			out.writeLong(ciId.longValue());
			out.writeLong(ciIdToCardIdMapping.get(ciId).longValue());
		}
		
		out.writeUTF(gameOtp);
	}

	/* ############################################################################################ */
	/* ############################################################################################ */
	/* ############################################################################################ */

	private List<Player> createPlayers(List<Long> playerIds,
			String[] otps, Map<Long, Integer> playerToDeckMapping, Map<Long, Integer> playerToOtpMapping) {
		
		List<Player> playerList = new ArrayList<Player>(playerIds.size());
		
		for (Long playerId : playerIds) {
			playerList.add(
					new Player(
							playerId,
							playerToDeckMapping.get(playerId).intValue(),
							otps[playerToOtpMapping.get(playerId).intValue()]
					)
			);
		}
		
		return playerList;
	}
	
	private void tellTCGManageableToCleanUp() {
		int pSize = getPlayers().size();
		long[] playerIds = new long[pSize];
		for (int i = 0; i < pSize; i++) {
			playerIds[i] = getPlayers().get(i).getPlayerId();
		}
		
		tcgAppManageable.cleanUp(getGameId(), playerIds, false);
	}
	
	/* ############################################################################################ */
	/* ############################################################################################ */
	/* ############################################################################################ */
	
	// These methods are properly synchronized with reference to 'memory consistency errors' (change visibility)
	// because they are called from the state's progress() method, which is externally synchronized by the
	// TCGApplication object.
	
	public TCGAppManageable getTCGAppManageable() {
		return tcgAppManageable;
	}
	
	public long getGameId() {
		return gameId;
	}
	
	public String getGameOtp() {
		return gameOtp;
	}

	public OnUserInteractionListener getGame() {
		return game;
	}
	
	public List<Player> getPlayers() {
		return players;
	}
	
	public Player getPlayerById(long playerId) {
		for(Player player : getPlayers()) {
			if (player.getPlayerId() == playerId) {
				return player;
			}
		}
		
		return null;
	}
	
	public Player getPlayerByWebSocket(WebSocket socket) {
		WebSocket playerSocket = null;
		for(Player player : getPlayers()) {
			playerSocket = player.getSocket();
			if (playerSocket != null && playerSocket == socket) {
				return player;
			}
		}
		
		return null;
	}
	
	public Player getPlayerByIndex(int index) {
		for(Player player : getPlayers()) {
			if (player.getIndex() == index) {
				return player;
			}
		}
		
		return null;
	}
	
	public Player getOpponent(Player player) {
		return getOpponent(player.getIndex());
	}
	
	public Player getOpponent(int playerIndex) {
		int i = playerIndex;
		
		for(Player p : getPlayers()) {
			if (i != p.getIndex()) {
				return p;
			}
		}
		
		return null;
	}
	
	public Player getActivePlayer() {
		return getPlayerByIndex(activePlayerIndex);
	}
	
	public boolean isReadyToPlay() {
		int i = 0;
		for (Player player : getPlayers()) {
			if (player.isConnected())
				i++;
		}
		
		return (i == GameConfig.NUMBER_OF_PLAYERS_PER_GAME);
	}
	
	public boolean isResumeRequired() {
		for(int i = 0; i < resumeRequired.length; i++) {
			if (resumeRequired[i])
				return true;
		}
		
		return false;
	}
	
	public void unsetResumeRequired(int playerIndex) {
		resumeRequired[playerIndex] = false;
	}
	
	
	/* ############################################################################################ */
	/* ############################################################################################ */
	/* ############################################################################################ */
	
	/* ############################################################################################ */
	/* ############################################################################################ */
	/* ############################################################################################ */

	/* ############################################################################################ */
	/* ############################################################################################ */
	/* ############################################################################################ */
	
	
	@Override
	public void onGameOver(int winner) {
		log.debug("onGameOver({})", winner);
		
		long winnerId = 0;
		if (winner == -1) {
			// draw
			winnerId = winner;
			GameReporter.broadcastGameProgress(this, winnerId);
		}
		else if (winner == 0 || winner == 1) {
			winnerId = getPlayerByIndex(winner).getPlayerId();
			GameReporter.broadcastGameProgress(this, winnerId);
		}
		else {
			assert false : "Invalid winner! Must be [ -1 | 0 |1 ]";
		}
		
		tcgAppManageable.signalGameOver(getGameId(), winnerId, getGameOtp());
		tellTCGManageableToCleanUp();
	}

	@Override
	public void onProgress(int player, int slot, long ciId, int[] propertyTypes, int life, int attack, int defense, int agility) {
		
		long cardId = -1;
		
		if (ciId != -1) {
			cardId = ciIdToCardIdMapping.get(ciId).longValue();
			log.debug("ciId {} -> cardId {}", ciId, cardId);
		}
		
		log.debug(
				"onProgress({}, {}, {}, {}, {}, {}, {}, {})",
				new Object[] { player, slot, cardId, Arrays.toString(propertyTypes), life, attack, defense, agility }
		);
		
		int[] pTypes = null;
		if (propertyTypes != null) {
			pTypes = propertyTypes;
		}
		else {
			pTypes = new int[0];
		}
		
		GameReporter.broadcastGameProgress(this, slot, getPlayerByIndex(player).getPlayerId(), cardId, pTypes, life, attack, defense, agility);
	}

	@Override
	public void onGameInfo(int type, String text) {
		log.debug("onGameInfo({}, {})", type, text);
		// TODO: 'text' needs to be 'msgKey' .... mit Martin absprechen
		GameReporter.broadcastGameInfo(this, text, type);
	}

	@Override
	public void onSelectionRequired(int player, int slot) {
		log.debug("onSelectionRequired({}, {})", player, slot);
		activePlayerIndex = player;
		
		// Notify activePlayer's opponent
		GameReporter.unicastGameProgress(getOpponent(player), TCGProgressFragment.Command.WAIT, slot);
		
		// Notify activePlayer
		GameReporter.unicastGameProgress(getPlayerByIndex(player), TCGProgressFragment.Command.SELECT, slot);
	}

	@Override
	public void onResumeRequired(int player) {
		log.debug("onResumeRequired({})", player);
		
		if (player == -1) {
			// Server requires a 'resume' from all players
			for(int i = 0; i < resumeRequired.length; i++) {
				resumeRequired[i] = true;
			}
			GameReporter.broadcastGameProgress(this, TCGProgressFragment.Command.RESUME);
		}
		else {
			// Server requires a 'resume' from the player with index 'player'
			resumeRequired[player] = true;
			GameReporter.unicastGameProgress(getPlayerByIndex(player), TCGProgressFragment.Command.RESUME);
		}
	}

}
